﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Scheduler.Business_Logic_Layer
{
    /**Class: ScoreSchedules
    * @author Victor Moy
    * @version 3.0
    * Course : ITEC 4860 Spring 2012
    * Written: March 22, 2012
    *
    *This class scores schedules based on how well they meet minimum and maximum shift lengths
     *and gets the best schedule
     *Will eventually put best schedule into DB as shifts
    * 
    *
    * Purpose: score and find best schedule
    */

    class ScoreSchedules
    {
        //current highest score, set to lowest value
        private double score = Double.MinValue;

        //best schedule
        private Dictionary<String, List<int>> bestScedule = new Dictionary<string, List<int>>();

        //holds schedules with employee names as keys and 30 min durations as values
        private List<Dictionary<string, List<string>>> invertedSchedules = new List<Dictionary<string, List<string>>>();

        //list of 30 min duration keys for other data structures
        private List<String> durationsKey;

        //List of all durations in form of time spans
        private List<TimeSpan> timeSpanKeys = new List<TimeSpan>();

        //Minimum shift in hours, hours*2
        //This is currently hard set for now,2 hours
        //A way to set the company's minimum shift was not made in the interface
        private int minHours = 4;

        //Maximum shift in hours, hours*2
        //This is currently hard set for now, 6 hours
        //A way to set the company's minimum shift was not made in the interface
        private int maxHours = 12;

        //creates sequencial list of durations
        private List<Dictionary<string, List<int>>> enumeratedSchedule = new List<Dictionary<string, List<int>>>();

        //employee preferred times
        private Dictionary<string, List<string>> preferredTimes = new Dictionary<string, List<string>>();

        //all schedules
        private List<Dictionary<String, List<string>>> schedules = new List<Dictionary<String, List<string>>>();

        //Scoring bonus constants and magnitudes, ordered from most important to least

        //Large Bonus for a shift that equals or exceeds minimum shift length
        private const int equalToOrGreaterThanMinimumHoursBonus = 5000;

        //Mediium Bonus for a shift between min and max hours. Less bonus the further away from the minimum
        //the shift is
        private const int betweeenMinimumAndMaximumHoursBonusMagnitude = 2000;

        //Bonus for matching employees' preferred times
        private const int employeePreferredTimeMetBonus = 25;


        //Scoring deduction constants and magnitudes, ordered from most important to least

        //Large Deduction for a shift is less than half the minimum hours
        private const int lessThanHalfMinimumHoursDeduction = 10000;

        //Medium point deduction for shift exceeding maximum hours
        private const int overMaximumHoursDeduction = 3000;

        //Medium point deduction for shift being less than minimum hours
        private const int lessThanMinimumHoursDeductionMagnitude = 1000;
    
        //Small point decudtion for people not being assigned even hours for the week
        private const int equalShiftDeductionMagnitude = 150;

        //days of the week
        List<string> daysOfWeekList;

        //allow the user to specify the date for the schedule
        private DateTime selectedDate;

        /** Method: ScoreSchedules
          * Constructor for this class. Instantiates all class variables
          * @param schedules all generated possible schedules
         * @param durationsKey all 30 min durations in a list
         * @param daysOfWeekList the names of the days of the week
         * @param preferredTimes all employees preferred times slots
          */
        public ScoreSchedules(List<Dictionary<String, List<string>>> schedules, List<String> durationsKey, List<string> daysOfWeekList, Dictionary<string, List<string>> preferredTimes,string selectedDate)
        {
            this.schedules = schedules;
            this.durationsKey = durationsKey;
            this.preferredTimes = preferredTimes;
            this.daysOfWeekList = new List<string>(daysOfWeekList);
            this.selectedDate = Convert.ToDateTime(selectedDate);
            fillTimeSpans();

            foreach (Dictionary<String, List<string>> schedule in schedules)
            {
                invertScheduleToKeysAndValues(schedule);
            }

            foreach (Dictionary<string, List<string>> invSched in invertedSchedules)
            {
                enumerateDurations(invSched);
            }

            for (int index = 0; index < schedules.Count; index++)
            {
                calculateScore(index);
            }

            createShifts();


        }

        /** Method: invertScheduleToKeysAndValues
          * Inverts the schedules to have empID as keys and 30 min durations as Values
         * param@ schedule is an individual schedule from the generated ones
         * */

        private void invertScheduleToKeysAndValues(Dictionary<String, List<string>> schedule)
        {
            Dictionary<string, List<string>> invertedSchedule = new Dictionary<string, List<string>>();
            foreach (Employee employee in Presentation_Layer.GlobalVariables.GlobalEmployeeList)
            {
                invertedSchedule.Add(employee.EmpID.Trim(), new List<string>());
                foreach (string key in schedule.Keys)
                {
                    if (schedule[key].Contains(employee.EmpID.Trim()))
                    {
                        invertedSchedule[employee.EmpID.Trim()].Add(key);
                    }
                }
            }
            invertedSchedules.Add(invertedSchedule);
        }


        /** Method: enumerateDurations
         * Matches 30 minutes shifts in invertedSchedules to index in all durations
         * to find the proper index of those iteams.
         * @param invertedSched is the invertedSchedules
        * */

        private void enumerateDurations(Dictionary<string, List<string>> invertedSched)
        {
            Dictionary<string, List<int>> tempSched = new Dictionary<string, List<int>>();

            foreach (string name in invertedSched.Keys)
            {
                List<int> tempEnumeratedDurations = new List<int>();
                foreach (string duration in invertedSched[name])
                {
                    tempEnumeratedDurations.Add(durationsKey.IndexOf(duration));
                }
                tempSched.Add(name, tempEnumeratedDurations);
            }

            enumeratedSchedule.Add(tempSched);
        }


        /** Method: calculateScore
         * Calculates scores of a schedule with index schedIndex
        * @param schedIndex indicates which schedule is being scores in Lists
         */

        private void calculateScore(int schedIndex)
        {
            //temporary score for the current schedule
            double tempScore = 0;


            Dictionary<string, List<int>> enumSched = enumeratedSchedule[schedIndex];

            Dictionary<string, List<string>> invSch = invertedSchedules[schedIndex];

            //counts length of a shift        
            int counter = 0;

            //stores shift lengths
            List<int> counterStorage = new List<int>();

            //variable to keep employee hours even
            int equalHoursCheck = 0;

            //iterates through each employee
            foreach (string name in enumSched.Keys)
            {
                if (enumSched[name].Count > 1)
                {
                    //Count up how many consequtive durations and save length as shift to be scored later in list
                    for (int index = 0; index < enumSched[name].Count - 2; index++)
                    {
                        if (enumSched[name].ElementAt(index) + 1 == enumSched[name].ElementAt(index + 1))
                        {
                            if (Convert.ToDouble(invSch[name].ElementAt(index + 1)) - Convert.ToDouble(invSch[name].ElementAt(index)) < GenerateAllPossibleSchedules.dayMagnitude)
                                counter++;
                        }
                        else
                        {
                            counterStorage.Add(counter);
                            counter = 0;
                        }

                    }

                    //tracks length of shifts
                    counterStorage.Add(counter);

                    //This checks how evenly assigned the hours are for each employee
                    //The greater the differences the larger the point deduction
                     if (equalHoursCheck != 0)
                     {
                        if (equalHoursCheck > (enumSched[name].Count))
                        {
                            tempScore = tempScore - ((equalHoursCheck - enumSched[name].Count) * equalShiftDeductionMagnitude);
                        }
                        if (equalHoursCheck < (enumSched[name].Count))
                        {
                            tempScore = tempScore - ((enumSched[name].Count - equalHoursCheck) * equalShiftDeductionMagnitude);
                            equalHoursCheck = (enumSched[name].Count);
                        }
                     }
                     else
                        equalHoursCheck = (enumSched[name].Count);

                }

                
                
                
            }

            //Scores all the shift lengths and adds those to this schedule's overall score
            foreach (int counted in counterStorage)
                {
                    //large penalty for shifts only half as long or less as the minimumNumber of Hours
                    if (counted <= minHours/2)
                    {
                        tempScore = tempScore - lessThanHalfMinimumHoursDeduction;
                    }

                    //medium penalty for shifts less than the minimum Number of Hours for shifts
                    //the greater the difference the bigger the deduciton
                    if (counted < minHours)
                    {
                        tempScore = tempScore - ((minHours - counted) * lessThanMinimumHoursDeductionMagnitude);
                    }

                    //Large Bonus for being equal to or greater than Minimum hours
                    if (counted >= minHours)
                    {
                        tempScore = tempScore + equalToOrGreaterThanMinimumHoursBonus;
                    }

                    //Medium bonus for shifts between min and max hours for shifts
                    //The points reduce the further from the minimum the shift is
                    if (counted > minHours && counted <= maxHours)
                    {
                        tempScore = tempScore + ((1.0 / (double)(counted - minHours)) * betweeenMinimumAndMaximumHoursBonusMagnitude);
                    }

                    //Large Penalty for shift exceeding maximum hours for shifts
                    if (counted > maxHours)
                    {
                        tempScore = tempScore - overMaximumHoursDeduction;
                    }

                    //Keeps best schedule according to score
                    /*if (tempScore > score)
                    {
                        
                        score = tempScore;
                        bestScedule = new Dictionary<string, List<int>>(enumSched);
                    }*/
                }

            
    
            //This calculates whether a duration is an employee's preferred time and adds a bonus to the score
            foreach (Employee employee in Presentation_Layer.GlobalVariables.GlobalEmployeeList)
            {
                foreach (string duration in preferredTimes[employee.EmpID.Trim()])
                {
                    if (invSch[employee.EmpID.Trim()].Contains(duration))
                    {
                        tempScore = tempScore + employeePreferredTimeMetBonus;
                    }
                }
            }

            //Keeps best schedule according to score
            if (tempScore > score)
            {
                score = tempScore;
                bestScedule = enumSched;
            }
        }

        /** Method: fillTimeSpans
         * Creates a timespan List matching the times in durationsKeys to be used in 
         * creating shifts
        * */

        private void fillTimeSpans()
        {
            for(int day = 0;day <7;day++)
            {
                for (int hour = 0; hour < 24; hour++)
                {
                    timeSpanKeys.Add(new TimeSpan(day,hour,0,0));
                    timeSpanKeys.Add(new TimeSpan(day,hour, 30, 0));
                }
            }
        }

        /** Method: createShifts
         * Uses Data_Access_Layer method to attempt creating a shifts from the best schedule
        * */

        private void createShifts()
        {
            //Check shifts of each employee
            foreach (string empID in bestScedule.Keys)
            {
                //indicates if this is the first time slot being checked
                bool startIndexSet = false;

                //start time index
                int startIndex = 0;

                //end time index
                int endIndex = 0;

                //Adds and records continuous shifts
                for (int index = 0; index < bestScedule[empID].Count;index++ )
                {
                    //writes shifts to database when done with an employee
                    if (index == bestScedule[empID].Count - 1)
                    {
                        endIndex = bestScedule[empID].ElementAt(index);
                        createOrUpdateShifts(startIndex, endIndex, empID);
                    }
                    else
                    {
                        //set first index if this is the first duration
                        if (startIndexSet == false)
                        {
                            startIndex = bestScedule[empID].ElementAt(index);
                            startIndexSet = true;
                        }

                        //compares value of current shift to value of next shift
                        if (bestScedule[empID].ElementAt(index) + 1 != bestScedule[empID].ElementAt(index + 1))
                        {
                            endIndex = bestScedule[empID].ElementAt(index);
                            createOrUpdateShifts(startIndex,endIndex,empID);
                            startIndex = bestScedule[empID].ElementAt(index + 1);
                        }
                    }
                }
                
            }
        }

        /// <summary>
        /// Method:createOrUpdateShifts
        /// Checks if shift is already in database and either updates or creates shifts accordingly
        /// </summary>
        /// <param name="startIndex">index of the start time</param>
        /// <param name="endIndex">index of the end time</param>
        /// <param name="empID">employee ID</param>
        /// 
        private void createOrUpdateShifts(int startIndex,int endIndex,string empID)
        {
            Data_Access_Layer.ScheduleDisplayer shifts =
                                new Data_Access_Layer.ScheduleDisplayer(selectedDate.StartOfWeek(DayOfWeek.Sunday).Add(timeSpanKeys[startIndex]), Presentation_Layer.GlobalVariables.GlobalCompanyID, empID);
            shifts.createSchedDay();
            
            if(shifts.locateShift() == false)
                shifts.createShift(new TimeSpan(timeSpanKeys[startIndex].Hours, timeSpanKeys[startIndex].Minutes, 0), new TimeSpan(timeSpanKeys[endIndex+1].Hours, timeSpanKeys[endIndex+1].Minutes, 0), false);
            else
                shifts.updateShift(selectedDate.StartOfWeek(DayOfWeek.Sunday).Add(timeSpanKeys[startIndex]), empID, new TimeSpan(timeSpanKeys[startIndex].Hours, timeSpanKeys[startIndex].Minutes, 0), new TimeSpan(timeSpanKeys[endIndex + 1].Hours, timeSpanKeys[endIndex + 1].Minutes, 0), false);
        }
    }

 }

